package com.swills.mandelbrot;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JLabel;
import javax.swing.JPanel;

public class MyCanvas extends JPanel {

	private static final int MAX_COLOR_RANGE = 255;
	private static final int MAX_RADIUS_SQRD = 4;
	private static final double MIN_RE = -2.5;
	private static final double MAX_RE = 1.5;
	private static final double MIN_IM = -1.2;
	private double minRe;
	private double maxRe;
	private double minIm;
	private double maxIm;
	private static final long serialVersionUID = 1L;
	private boolean doPaint = true;
	private Point mousePressedPnt = null;
	private Point mouseReleasePnt = null;
	private final JLabel minReLbl;
	private final JLabel maxReLbl;
	private final JLabel minImLbl;
	private final JLabel maxImLbl;
	private final JLabel complexLbl;

	public MyCanvas() {
		init();
		minReLbl = new JLabel();
		maxReLbl = new JLabel();
		complexLbl = new JLabel();
		minImLbl = new JLabel();
		maxImLbl = new JLabel();
		complexLbl.setForeground(Color.YELLOW);
		minReLbl.setForeground(Color.YELLOW);
		maxReLbl.setForeground(Color.YELLOW);
		minImLbl.setForeground(Color.YELLOW);
		maxImLbl.setForeground(Color.YELLOW);

		this.add(minReLbl);
		this.add(maxReLbl);
		this.add(minImLbl);
		this.add(maxImLbl);
		this.add(complexLbl);
		this.setBackground(Color.WHITE);

		this.addMouseListener(new MouseAdapter() {

			@Override
			public void mousePressed(MouseEvent e) {
				super.mousePressed(e);
				mousePressedPnt = e.getPoint();
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				super.mouseReleased(e);
				mouseReleasePnt = e.getPoint();
				calcComplexMinMax();
				repaint();
			}

			/**
			 * Calculate the new min and max range of the complex numbers based
			 * on a starting and ending set of points. Also used are the
			 * previous min max complex numbers.
			 */
			private void calcComplexMinMax() {
				int newX2;
				int newX1;
				if (mouseReleasePnt.x > mousePressedPnt.x) {
					newX1 = mousePressedPnt.x;
					newX2 = mouseReleasePnt.x;
				} else {
					newX1 = mouseReleasePnt.x;
					newX2 = mousePressedPnt.x;
				}

				int newY1;
				if (mouseReleasePnt.y > mousePressedPnt.y) {
					newY1 = mousePressedPnt.y;
				} else {
					newY1 = mouseReleasePnt.y;
				}
				double gap = maxRe - minRe;
				maxRe = minRe + gap * newX2 / (getWidth());
				minRe = minRe + gap * newX1 / (getWidth());
				minIm = minIm + (maxIm - minIm) * newY1 / (getHeight());
				maxIm = minIm + gap * getHeight() / getWidth();

			}

		});
	}

	public void init() {
		minRe = MIN_RE;
		maxRe = MAX_RE;
		minIm = MIN_IM;
	}

	@Override
	public void paintComponent(Graphics g) {
		super.paintComponent(g);

		if (doPaint) {
			Color[][] colorPoints = new Color[getWidth()][getHeight()];

			calcPoints(colorPoints);
			drawPoints(colorPoints, g);

			if (MyFrame.getVars().isShowAxis()) {
				drawAxis(g);
			}
		}
	}

	/**
	 * Draw re and im axis. Re axis from (minRe, 0) to (maxRe, 0). Im axis from
	 * (0, minIm) to (0, maxIm). Need to convert complex coords to point coords.
	 * CalcZeroCoordForRe given minRe, maxRe
	 */
	private void drawAxis(Graphics g) {
		// x-axis
		if (minIm <= 0 && maxIm >= 0) {
			int imZero = calcZeroCoord(minIm, maxIm, getHeight());
			g.setColor(Color.YELLOW);
			g.drawLine(0, imZero, getWidth() - 1, imZero);
		}

		// y-axis
		if (minRe <= 0 && maxRe >= 0) {
			int reZero = calcZeroCoord(minRe, maxRe, getWidth());
			g.setColor(Color.YELLOW);
			g.drawLine(reZero, 0, reZero, getHeight());
		}
	}

	public static int calcZeroCoord(double min, double max, int cartesionWidth) {
		if (min > 0 || max < 0) {
			throw new RuntimeException(
					"Invalid parameters to calculate zero position");
		}
		return (int) Math.round(Math.abs(min / (max - min) * cartesionWidth));
	}

	private void drawPoints(Color[][] colorPoints, Graphics g) {
		for (int x = 0; x < getWidth(); x++) {
			for (int y = 0; y < getHeight(); y++) {
				g.setColor(colorPoints[x][y]);
				g.drawLine(x, y, x, y);
			}
		}
	}

	private void calcPoints(Color[][] colorPoints) {
		int imgWidth = getWidth();
		int imgHeight = getHeight();
		double gap = maxRe - minRe;
		maxIm = minIm + gap * imgHeight / imgWidth;
		minReLbl.setText("Min Re = " + Double.toString(minRe));
		maxReLbl.setText("; Max Re = " + Double.toString(maxRe));
		minImLbl.setText("; Min Im = " + Double.toString(minIm));
		maxImLbl.setText("; Max Im = " + Double.toString(maxIm));

		double reFactor = gap / (imgWidth - 1);
		double imFactor = (maxIm - minIm) / (imgHeight - 1);

		for (int y = 0; y < imgHeight; y++) {
			double cIm = minIm + y * imFactor;// maxIm - y * imFactor;
			for (int x = 0; x < imgWidth; x++) {
				double cRe = minRe + x * reFactor;
				colorPoints[x][y] = calcManelbrotFunction(cRe, cIm);
			}
		}
	}

	private Color calcManelbrotFunction(double cRe, double cIm) {
		boolean isInside = true;
		double zRe = cRe;
		double zIm = cIm;
		int finalIteration = 0;
		int maxIters = MyFrame.getVars().getMaxIterations();
		// maxIters = 65;
		for (int i = 0; i < maxIters; i++) {
			finalIteration = i;
			double zRe2 = zRe * zRe;
			double zIm2 = zIm * zIm;
			if (zRe2 + zIm2 > MAX_RADIUS_SQRD) {
				isInside = false;
				break;
			}

			zIm = 2 * zRe * zIm + cIm;
			zRe = zRe2 - zIm2 + cRe;

		}
		if (isInside) {
			return Color.BLACK;
		} else {
			int half = (int) Math.ceil(maxIters / 2);
			if (finalIteration < half) {
				// ie 0 - 14 map to colours black (0,0,0) to red (255, 0, 0)
				return new Color(newColor(finalIteration), 0, 0);
			} else {
				int gbColor = 0;
				try {
					// ie 15 - 30 map to colours red (255,0,0) to white (255,
					// 255,
					// 255)
					gbColor = newColor(finalIteration - half);

					return new Color(255, gbColor, gbColor);
				} catch (RuntimeException e) {
					System.out.println("final iteration = " + finalIteration
							+ "; gbColor = " + gbColor + "; half = " + half);
					return new Color(255, 255, 255);
				}
			}
		}

	}

	public static int newColor(int finalIteration) {
		int iter = MyFrame.getVars().getMaxIterations();
		return Math.round(MAX_COLOR_RANGE * finalIteration * 2 / iter);
	}

	public void setDoPaint(boolean doPaint) {
		this.doPaint = doPaint;
	}

}
